"""
File: gui.py
Description: PyQt5 GUI for NCM to audio converter.
Provides a window to add folders, list .ncm files with metadata, and convert with progress bars.
All comments/docstrings are in English per requirement.
"""

import os
from typing import Dict, List, Optional

from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSettings
from PyQt5.QtGui import QPalette, QColor
from PyQt5.QtWidgets import (
    QApplication,
    QMainWindow,
    QWidget,
    QVBoxLayout,
    QHBoxLayout,
    QPushButton,
    QFileDialog,
    QTableWidget,
    QTableWidgetItem,
    QProgressBar,
    QMessageBox,
    QLabel,
    QCheckBox,
)

from converter import NCMConverter, scan_folder


class ConversionWorker(QThread):
    """Thread worker to convert a single .ncm file and report progress.

    Signals:
    - progress(float): percentage [0, 100]
    - result_ready(dict): result dict from converter.convert_file
    - failed(str): error message when exception occurs
    """

    progress = pyqtSignal(float)
    result_ready = pyqtSignal(dict)
    failed = pyqtSignal(str)

    def __init__(self, file_path: str, output_dir: Optional[str] = None):
        """Initialize worker with target file and output directory.

        Args:
            file_path: Path to .ncm file.
            output_dir: Optional output directory; defaults to source folder.
        """
        super().__init__()
        self.file_path = file_path
        self.output_dir = output_dir
        self.converter = NCMConverter()

    def run(self) -> None:
        """Execute conversion on this thread and emit progress updates."""
        try:
            def cb(p: float) -> None:
                self.progress.emit(p)

            result = self.converter.convert_file(
                self.file_path, output_dir=self.output_dir, progress_cb=cb
            )
            if result.get("success"):
                self.result_ready.emit(result)
            else:
                self.failed.emit(result.get("message", "Unknown error"))
        except Exception as e:
            self.failed.emit(str(e))


class MainWindow(QMainWindow):
    """Main application window for NCM to audio conversion.

    Layout:
    - Top toolbar with buttons: Add Folder, Convert All, Clear
    - Center table listing files: Name, Size, Format, Title, Artist, Output, Progress, Status
    - Status label at bottom for general messages
    Responsibilities:
    - Allow selecting folders to scan for .ncm files
    - Display file metadata and sizes
    - Convert files sequentially with visible progress bars
    - Handle worker lifecycle and update UI
    - Persist last used folders and support conversion selection & counts display
    """

    def __init__(self) -> None:
        """Initialize the main window and UI widgets."""
        super().__init__()
        self.setWindowTitle("NCM to MP3/FLAC Converter")
        self.resize(1000, 600)

        # Modern dark palette for tech feeling
        self._apply_dark_palette()
        # Apply Android-like (Material) high-contrast style for better readability
        self._apply_android_style()

        self._files: Dict[str, Dict] = {}
        self._row_by_path: Dict[str, int] = {}
        self._queue: List[str] = []
        self._current_worker: Optional[ConversionWorker] = None
        self._output_dir: Optional[str] = None

        # Settings to remember last selected folders
        self._settings = QSettings("ncm2mp3", "ncm2mp3")
        self._last_load_dir = self._settings.value("last_load_dir", os.path.expanduser("~"))
        self._last_output_dir = self._settings.value("last_output_dir", "")

        # Root widget and layouts
        root = QWidget(self)
        self.setCentralWidget(root)
        vbox = QVBoxLayout(root)

        # Toolbar
        toolbar = QHBoxLayout()
        self.btn_add = QPushButton("Add Folder")
        self.btn_convert = QPushButton("Convert All")
        self.btn_clear = QPushButton("Clear")
        self.btn_output = QPushButton("Output Folder")
        # Single option: output to source folder
        self.chk_use_source = QCheckBox("Use Source Folder")
        self.chk_use_source.setChecked(True)
        toolbar.addWidget(self.btn_add)
        toolbar.addWidget(self.btn_output)
        toolbar.addWidget(self.chk_use_source)
        toolbar.addWidget(self.btn_convert)
        toolbar.addWidget(self.btn_clear)
        toolbar.addStretch(1)
        vbox.addLayout(toolbar)
        # Disable output button when using source folder
        self.btn_output.setEnabled(not self.chk_use_source.isChecked())

        # Table
        self.table = QTableWidget(0, 8, self)
        self.table.setHorizontalHeaderLabels([
            "File Name",
            "Size (MB)",
            "Format",
            "Title",
            "Artist",
            "Output Path",
            "Progress",
            "Status",
        ])
        # Hide unused columns to simplify UI
        self.table.setColumnHidden(2, True)
        self.table.setColumnHidden(3, True)
        self.table.setColumnHidden(4, True)
        self.table.setColumnHidden(5, True)
        self.table.horizontalHeader().setStretchLastSection(True)
        self.table.setAlternatingRowColors(True)
        self.table.verticalHeader().setVisible(False)
        # Increase row height to avoid clipped text
        self.table.verticalHeader().setDefaultSectionSize(32)
        self.table.horizontalHeader().setHighlightSections(False)
        self.table.setSelectionBehavior(QTableWidget.SelectRows)
        vbox.addWidget(self.table)

        # Bottom bar: left counts, right status
        bottom_bar = QHBoxLayout()
        self.count_label = QLabel("Selected: 0  Remaining: 0")
        bottom_bar.addWidget(self.count_label)
        bottom_bar.addStretch(1)
        self.status_label = QLabel("Ready")
        bottom_bar.addWidget(self.status_label)
        vbox.addLayout(bottom_bar)

        # Connections
        self.btn_add.clicked.connect(self.on_add_folder)
        self.btn_convert.clicked.connect(self.on_convert_all)
        self.btn_clear.clicked.connect(self.on_clear)
        self.btn_output.clicked.connect(self.on_choose_output)
        self.chk_use_source.toggled.connect(self.on_use_source_toggled)
        # Update counts when selection changes
        try:
            self.table.selectionModel().selectionChanged.connect(lambda s, d: self._update_counts())
        except Exception:
            pass
        # Initial counts
        self._update_counts()

    def _add_row(self, path: str, info: Dict) -> None:
        """Add a new row to the table for the provided file info.

        Args:
            path: Absolute file path.
            info: Info dict containing name, size_bytes, meta.
        """
        row = self.table.rowCount()
        self.table.insertRow(row)
        self._row_by_path[path] = row

        size_mb = info.get("size_bytes", 0) / (1024 * 1024)
        meta = info.get("meta") or {}
        title = meta.get("musicName", "")
        fmt = meta.get("format", "")
        artists = meta.get("artist") or []
        artist_name = ", ".join(a[0] if isinstance(a, list) and a else str(a) for a in artists)

        self.table.setItem(row, 0, QTableWidgetItem(info.get("name", "")))
        self.table.setItem(row, 1, QTableWidgetItem(f"{size_mb:.2f}"))
        self.table.setItem(row, 2, QTableWidgetItem(fmt))
        self.table.setItem(row, 3, QTableWidgetItem(title))
        self.table.setItem(row, 4, QTableWidgetItem(artist_name))
        self.table.setItem(row, 5, QTableWidgetItem(""))

        prog = QProgressBar()
        prog.setRange(0, 100)
        prog.setValue(0)
        self.table.setCellWidget(row, 6, prog)
        self.table.setItem(row, 7, QTableWidgetItem("Pending"))

        # Store file path into row as data for later reference
        item = QTableWidgetItem(path)
        item.setFlags(Qt.ItemIsEnabled)  # read-only
        item.setToolTip(path)
        self.table.setVerticalHeaderItem(row, item)

    def on_add_folder(self) -> None:
        """Open a directory chooser, scan .ncm files, and populate the table."""
        folder = QFileDialog.getExistingDirectory(self, "Select Folder", self._last_load_dir)
        if not folder:
            return
        # Remember last load folder
        self._last_load_dir = folder
        try:
            self._settings.setValue("last_load_dir", folder)
        except Exception:
            pass
        found = scan_folder(folder)
        added = 0
        for path, info in found.items():
            if path not in self._files:
                self._files[path] = info
                self._add_row(path, info)
                added += 1
        self.status_label.setText(f"Added {added} file(s)")
        if added == 0:
            QMessageBox.information(self, "Info", "No new .ncm files found in this folder.")
        # Update counts after adding
        self._update_counts()

    def on_convert_all(self) -> None:
        """Start converting selected rows (if any) or all files sequentially with progress updates."""
        if self._current_worker is not None:
            QMessageBox.warning(self, "Warning", "Conversion is already in progress.")
            return
        # Build queue from selection; if none selected, convert all
        selected_rows = [idx.row() for idx in self.table.selectionModel().selectedRows()] if self.table.selectionModel() else []
        paths_order = list(self._files.keys())
        if selected_rows:
            # Map selected rows back to paths using _row_by_path reverse lookup
            selected_paths = []
            for p, r in self._row_by_path.items():
                if r in selected_rows:
                    selected_paths.append(p)
            # Keep table order for selected
            self._queue = [p for p in paths_order if p in set(selected_paths)]
        else:
            self._queue = paths_order
        if not self._queue:
            QMessageBox.information(self, "Info", ".ncm file list is empty.")
            return
        self.status_label.setText("Starting conversion...")
        self._update_counts()
        self._start_next()

    def _start_next(self) -> None:
        """Start next conversion job in the queue, if any."""
        if not self._queue:
            self.status_label.setText("All conversions completed.")
            # Popup notify when conversion ends
            QMessageBox.information(self, "Done", "All conversions completed.")
            self._update_counts()
            return
        path = self._queue.pop(0)
        row = self._row_by_path.get(path, -1)
        if row >= 0:
            self.table.setItem(row, 7, QTableWidgetItem("Converting"))
        # Respect single-option: use source folder when checked
        effective_output = None if self.chk_use_source.isChecked() else self._output_dir
        self._current_worker = ConversionWorker(path, output_dir=effective_output)
        self._current_worker.progress.connect(lambda p, r=row: self._update_progress(r, p))
        self._current_worker.result_ready.connect(lambda res, r=row: self._on_finished(r, res))
        self._current_worker.failed.connect(lambda msg, r=row: self._on_failed(r, msg))
        # Cleanup strictly after the QThread truly finishes
        self._current_worker.finished.connect(self._on_worker_done)
        self._current_worker.start()

    def _update_progress(self, row: int, percent: float) -> None:
        """Update progress bar in the table for a given row."""
        widget = self.table.cellWidget(row, 6)
        if isinstance(widget, QProgressBar):
            widget.setValue(int(percent))

    def _on_finished(self, row: int, result: Dict) -> None:
        """Handle successful conversion result and update row status.

        Args:
            row: Table row index.
            result: Result dict containing output_path and meta.
        """
        out = result.get("output_path", "")
        self.table.setItem(row, 5, QTableWidgetItem(out))
        self.table.setItem(row, 7, QTableWidgetItem("Done"))

    def _on_failed(self, row: int, message: str) -> None:
        """Handle a failed conversion and show error message."""
        self.table.setItem(row, 7, QTableWidgetItem("Failed"))
        QMessageBox.critical(self, "Error", message)

    def _on_worker_done(self) -> None:
        """Cleanup current worker and proceed to next job.
        Ensure the thread has finished to avoid 'QThread: Destroyed while thread is still running'.
        Also update remaining counts.
        """
        if self._current_worker is not None:
            try:
                self._current_worker.wait()
            except Exception:
                pass
        self._current_worker = None
        self._update_counts()
        self._start_next()

    def on_clear(self) -> None:
        """Clear table and internal file list."""
        if self._current_worker is not None:
            QMessageBox.warning(self, "Warning", "Cannot clear while converting.")
            return
        self.table.setRowCount(0)
        self._files.clear()
        self._row_by_path.clear()
        self._queue.clear()
        self.status_label.setText("Cleared")

    def on_choose_output(self) -> None:
        """Choose a global output folder; defaults to None (same as source)."""
        # Suggested initial dir: last output or last load dir
        init_dir = self._last_output_dir if self._last_output_dir else self._last_load_dir
        folder = QFileDialog.getExistingDirectory(self, "Select Output Folder", init_dir)
        if folder:
            self._output_dir = folder
            self._last_output_dir = folder
            try:
                self._settings.setValue("last_output_dir", folder)
            except Exception:
                pass
            self.status_label.setText(f"Output folder: {folder}")
        else:
            self._output_dir = None
            self.status_label.setText("Output folder: same as source")

    def on_use_source_toggled(self, checked: bool) -> None:
        """Toggle whether to output to source folder.

        Args:
            checked: True to use source folder, False to use selected output folder.
        """
        self.btn_output.setEnabled(not checked)
        if checked:
            self._output_dir = None
            self.status_label.setText("Output folder: same as source")
        else:
            # If user toggles to custom output and none selected yet, prompt immediately (optional)
            if not self._output_dir:
                # Do not force selection to keep minimal changes; user can click 'Output Folder'
                self.status_label.setText("Select an output folder or keep source.")

    def _update_counts(self) -> None:
        """Update bottom-left counts: selected rows and remaining jobs."""
        try:
            selected = len(self.table.selectionModel().selectedRows()) if self.table and self.table.selectionModel() else 0
        except Exception:
            selected = 0
        remaining = len(self._queue) + (1 if self._current_worker is not None else 0)
        self.count_label.setText(f"Selected: {selected}  Remaining: {remaining}")

    def _apply_dark_palette(self) -> None:
        """Apply a modern dark theme palette to the app for tech style."""
        pal = QPalette()
        pal.setColor(QPalette.Window, QColor(30, 30, 30))
        pal.setColor(QPalette.WindowText, QColor(220, 220, 220))
        pal.setColor(QPalette.Base, QColor(25, 25, 25))
        pal.setColor(QPalette.AlternateBase, QColor(35, 35, 35))
        pal.setColor(QPalette.ToolTipBase, QColor(220, 220, 220))
        pal.setColor(QPalette.ToolTipText, QColor(220, 220, 220))
        pal.setColor(QPalette.Text, QColor(220, 220, 220))
        pal.setColor(QPalette.Button, QColor(45, 45, 45))
        pal.setColor(QPalette.ButtonText, QColor(220, 220, 220))
        pal.setColor(QPalette.BrightText, QColor(255, 0, 0))
        pal.setColor(QPalette.Highlight, QColor(38, 79, 120))
        pal.setColor(QPalette.HighlightedText, QColor(0, 0, 0))
        QApplication.instance().setPalette(pal)


    def _apply_android_style(self) -> None:
        """Apply Android-like Material style via QSS for clear, modern UI."""
        # Accent color inspired by Material Design (teal or blue)
        accent = "#1E88E5"  # Blue 600
        accent_hover = "#2196F3"  # Blue 500
        accent_pressed = "#1565C0"  # Blue 800
        bg = "#121212"  # Dark background
        surface = "#1F1F1F"
        text_primary = "#FFFFFF"
        text_secondary = "#B0B0B0"
        qss = f"""
        QMainWindow, QWidget {{
            background-color: {bg};
            color: {text_primary};
            font-size: 12pt;
        }}

        QLabel {{
            color: {text_secondary};
        }}

        QPushButton {{
            background-color: {accent};
            color: {text_primary};
            border: none;
            border-radius: 6px;
            padding: 8px 14px;
            font-weight: 600;
        }}
        QPushButton:hover {{
            background-color: {accent_hover};
        }}
        QPushButton:pressed {{
            background-color: {accent_pressed};
        }}
        QPushButton:disabled {{
            background-color: #2c2c2c;
            color: #777777;
        }}

        QTableWidget {{
            background-color: {bg};
            alternate-background-color: {surface};
            color: {text_primary};
            gridline-color: #333333;
            selection-background-color: {accent};
            selection-color: {text_primary};
        }}
        QHeaderView::section {{
            background-color: {surface};
            color: {text_secondary};
            padding: 8px;
            border: none;
            border-bottom: 1px solid #2a2a2a;
            font-weight: 600;
        }}

        QProgressBar {{
            background: {surface};
            border: 1px solid #2a2a2a;
            border-radius: 6px;
            text-align: center;
            color: {text_primary};
        }}
        QProgressBar::chunk {{
            background-color: {accent};
            border-radius: 6px;
        }}
        """
        QApplication.instance().setStyleSheet(qss)

    def closeEvent(self, event) -> None:
        """Ensure background worker thread is finished before closing the window.
        This prevents runtime warning or crash when the application exits during an active thread.
        """
        if self._current_worker is not None:
            try:
                self.status_label.setText("Waiting for worker to finish...")
                self._current_worker.wait()
            except Exception:
                pass
            finally:
                self._current_worker = None
        event.accept()